<!DOCTYPE html>
<!-- Copyright © 2021~2022 by wzh -->
<html lang="zh">
	<head>
		<meta charset="UTF-8" />
		<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport" />
		<title>联机对战 | 4D五子棋</title>
		
		<!--icon-->
		<link href="./img/icon.png" rel="icon" type="image/x-icon"/>
		
		<!-- 异步fonts加载 -->
		<script src="./js/webfontloader.js"></script>
		<script>
		if (typeof require != "undefined"){ //electron
			console.log("electron")
			document.write(`<link href="./css/fonts.css" rel="stylesheet" type="text/css" />`);
			window.onload = function(){
				console.log("onload")
				$("body").addClass("wf-active");
			}
		}else{
			WebFont.load({
				google: {
					families: ["kt", "st", "xs", "zyyt"],
					api: "./css/fonts.css"
				}
			});
		}
		</script>
		
		<style>
		/* 字体缩放方案 */
		/* @media screen and (max-width:320px){
			body {font-size: calc(0.018 * 320px + 6px) !important;}
		}
		body{
			font-size: calc(1.8vw + 6px) !important;
		}
		@media screen and (min-width: 960px){
			body {font-size: calc(0.018 * 960px + 6px) !important;}
		} */
		
		/* element */
		*{
			outline: none; /* 无点击线 */
		}
		html{
			/* 禁止选择 */
			-moz-user-select: none;
			-khtml-user-select: none;
			user-select: none;
			
			background-color: #fffae8; /* 背景 */
		}
		html.outDown{
			min-height: 100%;
			background: #fffae8 url("./img/background.jpg") no-repeat fixed center;
			background-size: cover;
			max-width: 100%;
			margin: auto;
			
			overflow-y: hidden;
		}
		
		body{
			position: absolute;
			width: 100%;
			/*height: 100%;*/
			left: 0;
			top: 0;
			margin: 0;
			
			display: flex;
			flex-direction: column;
			
			background-color: #fffae8; /* 背景 */
			
			opacity: 0; /* 完全透明 */
			transition: opacity 1s, top 0.6s; /* 出入动画 */
		}
		html.fadeIn > body{
			opacity: 1; /* 不透明 */
		}
		html.outDown > body{
			top: 100%; /* 下滑 */
			overflow-y: hidden;
		}
		
		
		/* header */
		body > header{
			flex: none;
			display: flex;
			width: 100%;
			z-index: 1;
			background-color: #aef;
		}
		body > header > i{
			flex: none;
			padding: 0.3rem;
			font-size: 2em !important;
			font-weight: bold;
			
			background-color: #aef;
			border: none;
			transition: background-color 0.3s;
		}
		body > header > i:focus{
			background-color: #afe;
		}
		body > header > i:active{
			background-color: #9ed;
		}
		body > header > h1{
			display: inline-block;
			width: 100%;
			margin: 0;
			padding: 6px 0;
			text-align: center;
			font-weight: bold;
		}
		
		
		/* state */
		#state{
			z-index: 1;
			flex: none;
			margin: 0;
			padding: 0;
			text-align: center;
		}
		#state.connecting{
			background-color: yellow;
			color: red;
		}
		#state.success{
			/*display: none;*/
			background-color: blue;
			color: white;
		}
		#state.failed{
			background-color: red;
			color: white;
		}
		
		
		/* main */
		#main{
			margin: 8px;
			
			display: flex;
			flex-direction: column;
		}
		
		/* 双方 */
		#main > div.side{
			display: flex;
			align-items: center;
			z-index: 1;
		}
		#main > div.opposite{
			text-align: left;
			justify-content: flex-start;
		}
		#main > div.self{
			text-align: right;
			justify-content: flex-end;
		}
		
		#main > div.side > i{ /* 图标 */
			display: inline-block;
			width: 1.8rem;
			height: 1.8rem;
			margin: 0.1rem;
			border-radius: 50%;
		}
		#main > div.black > i{ /* 黑 */
			background-color: black;
			border: 1px solid white;
		}
		#main > div.white > i{ /* 白 */
			background-color: white;
			border: 1px solid black;
		}
		
		#main > div.side > div{
			display: inline-flex;
			flex-direction: column;
		}
		
		#main > div.side > div > p.name{ /* 名称 */
			display: inline-block;
			margin: 0.1rem;
			font-weight: bold; /* 粗体 */
		}
		#main > div.black > div > p.name:before{ /* 黑 */
			content: "黑方";
		}
		#main > div.white > div > p.name:before{ /* 白 */
			content: "白方";
		}
		
		#main > div.side > div > p.time{ /* 时长 */
			display: inline-block;
			margin: 0.1rem;
		}
		
		/* 棋盘 */
		#board{
			position: fixed;
			display: block;
			left: 0;
			top: 0;
			width: 100%;
			height: 100%;
		}
		html.outDown #board{
			position: absolute;
		}
		#main > .fill{
			flex: none;
		}
		
		/* 控制 */
		#layerControl{ /*显示层控制*/
			position: absolute;
			left: 1rem;
			top: calc(100vh - 10rem);
			z-index: 1;
			display: flex;
			flex-direction: column;
			
			border: 1px solid #000;
			border-radius: 0.3rem;
			/*background-color: #fffae8;*/
		}
		#layerControl > *{
			padding: 0.3rem;
			font-size: 1.2em;
		}
		#layerControl > *:first-child{
			border-bottom: 1px solid #000;
		}
		#layerControl > *:active{
			background-color: #eee;
		}
		#heightControl{
			position: absolute;
			right: 1rem;
			top: calc(100vh - 10rem);
			z-index: 1;
			display: flex;
			flex-direction: column;
			
			border: 1px solid #000;
			border-radius: 0.3rem;
			/*background-color: #fffae8;*/
		}
		#heightControl > *{
			padding: 0.3rem;
			font-size: 1.2em;
		}
		#heightControl > *:first-child{
			border-bottom: 1px solid #000;
		}
		#heightControl > *:active{
			background-color: #eee;
		}
		
		/* 4D坐标轴 */
		#axis{
			position: absolute;
			height: 50vh;
			top: calc(100vh - 10rem - 1.2em * 2 - 50vh);
			z-index: 1;
		}
		
		#axis > .scale{
			position: absolute;
			height: 100%;
			display: flex;
			flex-direction: column;
			justify-content: space-around;
			margin: 0;
			padding: 0;
		}
		#axis > .scale > li{
			display: inline-block;
			margin: 0;
			padding: 0;
			height: 6px;
			width: 20px;
			border: 1px solid #000;
			border-radius: 999vw;
			background-color: white;
			transition: width 0.6s, background-color 0.6s;
		}
		#axis > .scale > li.win{
			background-color: #f00;
			width: 26px;
		}
		#axis > .scale > li.play{
			background-color: #0f0;
			width: 30px;
		}
		#axis > .scale > li.in{
			background-color: #00f;
			width: 40px;
		}
		
		#axis > .arrows{
			position: absolute;
			width: 6px;
			height: 100%;
		}
		#axis > .arrows > em{
			display: block;
			width: 100%;
			height: 100%;
			/* border: 1px solid #000; */
			border-radius: 999vw;
			background-color: #fff;
		}
		#axis > .arrows > i:first-of-type{
			position: absolute;
			display: block;
			width: 100%;
			height: 15px;
			/* z-index: -1; */
			/* border: 1px solid #000; */
			border-radius: 999vw;
			background-color: #fff;
			transform-origin: center top;
			transform: rotate(150deg);
		}
		#axis > .arrows > i:last-of-type{
			position: absolute;
			display: block;
			width: 100%;
			height: 15px;
			/* z-index: -1; */
			/* border: 1px solid #000; */
			border-radius: 999vw;
			background-color: #fff;
			transform-origin: center top;
			transform: rotate(-150deg);
		}
		#axis > .arrows > span{
			font-size: 1.2em;
			font-weight: bold;
			color: #fff;
		}
		
		
		/* 工具按钮 */
		#toolBtn{
			z-index: 1;
		}
		#toolBtn > button{
			width: 2.8rem;
			height: 2.8rem;
			margin: 0.1rem;
			font-size: 1em;
			
			background-color: #fc7;
			border: 1px solid #c94;
			border-radius: 50%;
			transition: background-color 0.3s;
			box-shadow: 1px 1px 2px #888,
				3px 3px 2px #888;
		}
		#toolBtn > button:active{
			background-color: #eb6;
			box-shadow: 3px 3px 3px #888;
		}
		
		/* 工具 */
		#tools{
			z-index: 1;
		}
		
		#tools > .scores{
			border: 1px solid #ddd;
		}
		
		#tools > .review > *{
			display: inline-block;
			width: 2rem;
			height: 2rem;
			margin: 0.2rem;
			padding: 0.3rem;
			background-color: #ddd;
			border: 3px outset #ddd;
			box-shadow: 1px 1px 2px #888,
				3px 3px 2px #888;
		}
		#tools > .review > *:active{
			background-color: #ccc;
			border: 3px inset #ddd;
		}
		
		
		/* info */
		#info{
			z-index: 1;
			display: flex;
			flex-direction: column;
			margin: 8px;
		}
		#info > .output{
			height: 160px;
			overflow-y: auto;
		}
		#info > .output > p{
			margin: 0;
			font-size: 0.9em;
		}
		#info > .input{
			display: flex;
			margin-top: 0.5rem;
		}
		#info > .input > input{
			width: 100%;
			margin-right: 0.2rem;
		}
		#info > .input > button{
			flex: none;
			margin-left: 0.2rem;
		}
		</style>
		
		<!-- jquery -->
		<!--<script src="./js/jquery.min.js"></script>
		<script>
		if (typeof require != "undefined") //electron
			window.$ = window.jQuery = require("./js/jquery.min.js");
		</script>-->
		
		<!-- 语言包 -->
		<script src="./lang/lang.js" defer></script>
		
		<!-- babylon.js -->
		<script src="./js/babylon.min.js"></script>
		<script src="./js/babylonjs.materials.min.js"></script>
		
		<!-- layui -->
		<!--<link rel="stylesheet" href="https://unpkg.com/layui@2.6.4/dist/css/layui.css">
		<script src="https://unpkg.com/layui@2.6.8/dist/layui.js"></script>-->
		<script src="./layui/layui.js"></script>
		<link rel="stylesheet" href="./layui/css/layui.css" />
		
		<!-- 调试工具 -->
		<!-- <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
		<script src="https://cdn.bootcss.com/vConsole/3.3.4/vconsole.min.js"></script>
		<script>
		if ( /ipad|iphone|midp|rv:1.2.3.4|ucweb|android|windows ce|windows mobile/.test( navigator.userAgent.toLowerCase() ) ){
			//手机
			eruda.init();
			new VConsole();
		}
		</script> -->
		
		<script src="./js/js_plus.min.js"></script> <!-- js+ -->
		
		<!-- PianoMusic Bgm -->
		<script src="./js/acoustic_grand_piano-ogg.js"></script>
		<script src="./js/music.js"></script>
		<script src="./js/bgm.js"></script>
		
		<script src="https://wzh.glitch.me/socket.io/socket.io.js"></script> <!-- socket -->
		
		<script src="https://wzh656.github.io/gobang/js/dynamic.js" async></script>
		<script src="https://wzh656.github.io/gobang/js/count.js" async></script>
		<script>
		var _hmt = _hmt || [];
		(function() {
			var hm = document.createElement("script");
			hm.src = "https://hm.baidu.com/hm.js?c6b8f0ad7ec671e54d83e86a67280999";
			var s = document.getElementsByTagName("script")[0]; 
			s.parentNode.insertBefore(hm, s);
		})();
		</script>
		
		<!-- Global site tag (gtag.js) - Google Analytics -->
		<script async src="https://www.googletagmanager.com/gtag/js?id=G-WTWJEHK1C2"></script>
		<script>
		window.dataLayer = window.dataLayer || [];
		function gtag(){dataLayer.push(arguments);}
		gtag('js', new Date());
		
		gtag('config', 'G-WTWJEHK1C2');
		</script>
		
	</head>
	<body>
		<header id="header">
			<i id="back" class="layui-icon layui-icon-return"></i>
			<h1>联机对战 4D</h1>
			<i id="restart" class="layui-icon layui-icon-refresh"></i>
		</header>
		<p id="state" class="connecting">连接服务器中……</p>
		<section id="main">
			<div class="side opposite">
				<i></i>
				<div>
					<p class="name"></p>
					<p class="time">
						<span>局时：</span><span class="total">15:00</span> &nbsp;
						<span>步时：</span><span class="step">60s</span>
					</p>
				</div>
			</div>
			<canvas id="board"></canvas>
			<div class="fill"></div>
			<div id="axis">
				<ul class="scale"></ul>
				<div class="arrows">
					<em></em>
					<i></i>
					<i></i>
					<span>W</span>
				</div>
			</div>
			<div id="layerControl">
				<i class="add layui-icon layui-icon-addition"></i>
				<i class="sub layui-icon layui-icon-subtraction"></i>
			</div>
			<div id="heightControl">
				<i class="add layui-icon layui-icon-up"></i>
				<i class="sub layui-icon layui-icon-down"></i>
			</div>
			<div class="side self">
				<div>
					<p class="name"></p>
					<p class="time">
						<span>局时：</span><span class="total">15:00</span> &nbsp;
						<span>步时：</span><span class="step">60s</span>
					</p>
				</div>
				<i></i>
			</div>
			<div id="toolBtn">
				<button class="giveUp">认输</button>
				<button class="drawMatch">求和</button>
				<button class="scores">棋局<br>评分</button>
				<button class="analyse">棋局<br>分析</button>
				<button class="review" disabled>复盘<br>回放</button>
			</div>
			<div id="tools">
				<canvas class="scores" height="36" style="display: none;"></canvas>
				<div class="review" style="display: none;">
					<svg class="last" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
						<polygon points="94.65,10 25.35,50 94.65,90 94.65,10" />
						<polygon points="5.35,10 25.35,10 25.35,90 5.35,90" />
					</svg>
					<svg class="play" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
						<polygon points="6.7,0 93.3,50 6.7,100 6.7,0" />
					</svg>
					<svg class="stop" style="display: none;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
						<polygon points="20,0 40,0 40,100 20,100" />
						<polygon points="60,0 80,0 80,100 60,100" />
					</svg>
					<svg class="next" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" xmlns:xlink="http://www.w3.org/1999/xlink">
						<polygon points="5.35,10 74.65,50 5.35,90 5.35,10" />
						<polygon points="94.65,10 74.65,10 74.65,90 94.75,90" />
					</svg>
				</div>
			</div>
			<form id="info" onsubmit="return chat();">
				<div class="output layui-textarea" disabled></div>
				<div class="input">
					<input placeholder="要发送的消息" class="layui-input" />
					<button class="send layui-btn layui-btn-primary layui-border-blue">发送</button>
				</div>
			</form>
		</section>
		
<script>
function resize(){
	console.log("resize")
	
	const width = innerWidth-16,
		height = innerHeight
			-$("#header").height()
			-8*2 //#main margin
			-$("#main > .side").height()*2
			-$("#toolBtn").height();
	$("#main > .fill").css("width", width + "px")
		.css("height", height + "px");
	
	$("#tools > .scores").attr("width", innerWidth-18);
}
resize();
$(window).resize(function(){
	$("#board").attr("width", innerWidth)
		.attr("height", innerHeight);
	
	if (board.engine)
		board.engine.resize(); //更新大小
});

document.addEventListener("plusready", function(){
	plus.navigator.setStatusBarBackground("#aef");
	plus.navigator.setStatusBarStyle("dark");
	plus.key.addEventListener("backbutton", function(){
		go("room.html");
	});
}, false);
</script>
<script>
$("html").addClass("fadeIn");
function go(url){
	$("html").addClass("outDown");
	setTimeout(function(){
		if (typeof plus != "undefined"){
			plus.navigator.setStatusBarBackground("#000");
			plus.navigator.setStatusBarStyle("light");
		}
		location.href = url;
	}, 600);
	layer.load(0);
}


$("button, input[type='button'], svg, header > i, #layerControl *, #heightControl *").click(function(){
	playEffect(); //播放音效
});



class Board{
	constructor ({canvas, background, rows, columns, depth, width, none, mistake, black, white, winLines, spacing}){
		this.canvas = canvas;
		this.width = canvas.width;
		this.height = canvas.height;
		this.background = background;
		
		this.columns = columns;
		this.rows = rows;
		this.depth = depth;
		this.width = width;
		
		this.columnRange = [0, columns-1]; //列范围
		this.rowRange = ~~((rows-1)/2); //行
		this.depthRange = [0, depth-1]; //深度范围
		this.widthRange = ~~((width-1)/2); //四维长度
		
		this.none = none; //空气配置
		this.mistake = mistake; //防误触配置
		this.black = black; //黑棋配置
		this.white = white; //白棋配置
		this.winLines = winLines; //获胜划线材质
		
		this.spacing = spacing; //间隔
		this.lines = { //划线
			border: [], //棋盘边界
			circle: [], //圆圈标记
			update: { //需更新的划线
				win: [],
				analyse: [],
				circle: []
			},
			win: [], //获胜
			analyse: [] //分析
		};
	}
	
	
	/* 场景 */
	//载入场景
	loadScene(skyboxDisplay=true, moonDisplay=true, borderDisplay=true){
		//引擎 场景 相机
		this.engine = new BABYLON.Engine(this.canvas); //引擎
		this.engine.enableOfflineSupport = false; //不支持离线 以减少内存
		this.scene = new BABYLON.Scene(this.engine, {
			//useGeometryIdsMap: true,
			//useMaterialMeshMap: true,
			//useClonedMeshMap: true
		}); //场景
		this.scene.clearColor = new BABYLON.Color3(...this.background); //背景颜色
		this.camera = new BABYLON.ArcRotateCamera( //弧形旋转相机
			"Camera",
			-Math.PI/2, //alpha=-90°
			Math.PI/4, //beta=45°
			300, //radius
			BABYLON.Vector3.Zero(), //position
			this.scene
		);
		this.camera.attachControl(this.canvas, false);
		//删除默认按键
		this.camera.inputs.attached.keyboard.keysUp = [];
		this.camera.inputs.attached.keyboard.keysDown = [];
		this.camera.inputs.attached.keyboard.keysLeft = [];
		this.camera.inputs.attached.keyboard.keysRight = [];
		
		//添加光
		this.hemisphericLight = new BABYLON.HemisphericLight( //模拟环境光
			"hemisphericLight",
			new BABYLON.Vector3(0, 1, 0),
			this.scene
		);
		this.directionalLight = new BABYLON.DirectionalLight( //平行光
			"directionalLight",
			new BABYLON.Vector3(0.6, 1, 0.6), //方向（斜向上）
			this.scene
		);
		this.directionalLight.intensity = 0.6; //亮度
		
		//所有棋子模板
		this.materials = {}; //材质
		this.templates = {}; //网格模板
		for (const name of ["none", "mistake", "black", "white"]){
			const material = new BABYLON.StandardMaterial("material_"+name, this.scene); //空气材质
			if ( this[name].color[0] )
				material.diffuseColor = new BABYLON.Color3(...this[name].color[0]); //漫反射颜色
			if ( this[name].color[1] )
				material.specularColor = new BABYLON.Color3(...this[name].color[1]); //镜面颜色
			if ( this[name].color[2] )
				material.emissiveColor = new BABYLON.Color3(...this[name].color[2]); //自发光颜色
			if ( this[name].color[3] )
				material.ambientColor = new BABYLON.Color3(...this[name].color[3]); //环境光颜色
			material.freeze();
			this.materials[name] = material;
			
			let template;
			if (name == "none" || name == "mistake"){ //方块
				template = BABYLON.MeshBuilder.CreateBox(
					"piece_template_"+name,
					{size: this[name].size}, //边长
					this.scene
				);
			}else{ //球
				template = BABYLON.MeshBuilder.CreateSphere(
					"piece_template_"+name,
					{diameter: this[name].size}, //边长
					this.scene
				);
			}
			template.material = material;
			template.isVisible = false; //隐藏
			template.freezeWorldMatrix();
			this.templates[name] = template;
		}
		//获胜划线材质
		const material = new BABYLON.StandardMaterial("material_winLines", this.scene); //空气材质
		if ( this.winLines.color[0] )
			material.diffuseColor = new BABYLON.Color3(...this.winLines.color[0]); //漫反射颜色
		if ( this.winLines.color[1] )
			material.specularColor = new BABYLON.Color3(...this.winLines.color[1]); //镜面颜色
		if ( this.winLines.color[2] )
			material.emissiveColor = new BABYLON.Color3(...this.winLines.color[2]); //自发光颜色
		if ( this.winLines.color[3] )
			material.ambientColor = new BABYLON.Color3(...this.winLines.color[3]); //环境光颜色
		material.freeze();
		this.materials.winLines = material;
		
		
		//通用动画缓动函数
		this.easingFunction = new BABYLON.CircleEase(); //缓动函数
		this.easingFunction.setEasingMode( BABYLON.EasingFunction.EASINGMODE_EASEINOUT ); //模式
		
		
		//生成所有占位符和棋子
		this.places = Array.new(null, this.columns, this.depth);
		this.pieces = Array.new(null, this.columns, this.rows, this.depth, this.width);
		//this.clear();
		
		
		//生成棋盘边界
		if (borderDisplay){ //允许显示
			const paths = [];
			paths[0] = [ //上面
				new BABYLON.Vector3(
					...this.ijk2xyz(0, 0, 0)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, 0, 0)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, 0, this.depth-1)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(0, 0, this.depth-1)
				),
				new BABYLON.Vector3( //闭合
					...this.ijk2xyz(0, 0, 0)
				)
			];
			paths[1] = [ //下面
				new BABYLON.Vector3(
					...this.ijk2xyz(0, this.rows-1, 0)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, this.rows-1, 0)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, this.rows-1, this.depth-1)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(0, this.rows-1, this.depth-1)
				),
				new BABYLON.Vector3( //闭合
					...this.ijk2xyz(0, this.rows-1, 0)
				)
			];
			paths[2] = [
				new BABYLON.Vector3(
					...this.ijk2xyz(0, 0, 0)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(0, this.rows-1, 0)
				)
			];
			paths[3] = [
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, 0, 0)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, this.rows-1, 0)
				),
			];
			paths[4] = [
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, 0, this.depth-1)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(this.columns-1, this.rows-1, this.depth-1)
				),
			];
			paths[5] = [
				new BABYLON.Vector3(
					...this.ijk2xyz(0, 0, this.depth-1)
				),
				new BABYLON.Vector3(
					...this.ijk2xyz(0, this.rows-1, this.depth-1)
				)
			];
			const color = new BABYLON.Color4(0.8, 0.8, 0, 1); //黄色
			for (const path of paths){
				const colors = new Array(path.length).fill(color);
				const line = BABYLON.MeshBuilder.CreateLines("lines", {points: path, colors}, this.scene);
				line.isPickable = false; //禁止点击
				this.lines.border.push(line);
				//this.sizeInAnimation(line, 100);
			}
		}
		
		
		//天空盒
		if (skyboxDisplay){ //允许显示
			const skybox = BABYLON.Mesh.CreateBox("skyBox", 3000, this.scene);
			const skyMaterial = new BABYLON.SkyMaterial("skyMaterial", this.scene);
			skyMaterial.backFaceCulling = false;
			skybox.material = skyMaterial;
			skybox.isPickable = false; //禁止点击
			this.skybox = skybox;
			
			//太阳倾斜角
			const skyInclinationAnimation = new BABYLON.Animation(
				"animation_sky_inclination",
				"material.inclination",
				30,
				BABYLON.Animation.ANIMATIONTYPE_FLOAT,
				BABYLON.Animation.ANIMATIONLOOPMODE_RELATIVE
			);
			skyInclinationAnimation.setKeys([
				{frame: 0, value: -0.5},
				{frame: 30*60, value: 0.5}
			]);
			//太阳浑浊度
			const skyTurbidityAnimation = new BABYLON.Animation(
				"animation_sky_turbidity",
				"material.turbidity",
				30,
				BABYLON.Animation.ANIMATIONTYPE_FLOAT,
				BABYLON.Animation.ANIMATIONLOOPMODE_RELATIVE
			);
			skyTurbidityAnimation.setKeys([
				{frame: 0, value: 2},
				{frame: 30*30, value: 5},
				{frame: 30*60, value: 2}
			]);
			//天空亮度
			const skyLuminanceAnimation = new BABYLON.Animation(
				"animation_sky_luminance",
				"material.luminance",
				30,
				BABYLON.Animation.ANIMATIONTYPE_FLOAT,
				BABYLON.Animation.ANIMATIONLOOPMODE_RELATIVE
			);
			skyLuminanceAnimation.setKeys([
				{frame: 0, value: 1},
				{frame: 30*30, value: 0.5},
				{frame: 30*60, value: 1}
			]);
			skybox.animations = [skyInclinationAnimation, skyTurbidityAnimation, skyLuminanceAnimation];
			this.scene.beginAnimation(skybox, 0, 30*60, true);
			
			setInterval(function(){
				const x = (skyMaterial.inclination + 1) % 2,
					y1 = 256/(1+Math.E**( -4*Math.E*(x-0.5) )),
					y2 = 256/(1+Math.E**( 4*Math.E*(x-1.5) )),
					min = ~~(256 - Math.min(y1, y2)),
					color = `rgb(${min},${min},${min})`;
				$("#main").css("color", color);
				$("#layerControl, #heightControl, #layerControl > *, #heightControl > *")
					.css("border-color", color);
			}, 1000);
			
			//月亮
			if (moonDisplay){
				const spriteManager = new BABYLON.SpriteManager("moonManager", "./img/moon.png", 1, 1059, this.scene);
				const moon = new BABYLON.Sprite("moon", spriteManager);
				moon.size = 20;
				this.moon = moon;
				setInterval(function(){
					if ((skyMaterial.inclination+0.5)%2 < 1)
						return (moon.isVisible = false);
					const radius = 500, //轨道距离
						theta = (skyMaterial.inclination+1.5)%2 /2 *2*Math.PI;
					moon.isVisible = true;
					moon.position.set(
						0,
						radius * Math.sin(theta),
						radius * Math.cos(theta)
					);
				}, 33);
			}
		}
		
		
		//优化阻止dirty
		this.scene.blockMaterialDirtyMechanism = true;
		
		//渲染循环
		this.engine.runRenderLoop(()=>{
			//this.stats.update(); //stats.js更新
			this.scene.render();
		});
	}
	
	//棋子由小变大动画
	sizeInAnimation(mesh, frame=20){
		return new Promise((resolve, reject)=>{
			mesh.goalVisible = true; //将要显示
			mesh.isVisible = true;
			if (mesh.scaling.x == 1 && mesh.scaling.y == 1 && mesh.scaling.z == 1)
				mesh.scaling.set(0, 0, 0);
			//由小到大动画
			BABYLON.Animation.CreateAndStartAnimation(
				"animation_sizeIn",
				mesh,
				"scaling",
				30,
				frame,
				mesh.scaling,
				new BABYLON.Vector3(1, 1, 1),
				BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT,
				this.easingFunction,
				resolve
			);
		});
	}
	
	//棋子由大变小动画
	sizeOutAnimation(mesh, frame=20){
		return new Promise((resolve, reject)=>{
			//由大到小动画
			mesh.goalVisible = false; //将要隐藏
			BABYLON.Animation.CreateAndStartAnimation(
				"animation_sizeOut",
				mesh,
				"scaling",
				30,
				20,
				mesh.scaling,
				new BABYLON.Vector3(0, 0, 0),
				BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT,
				this.easingFunction,
				()=>{
					if (!mesh.goalVisible) //仍要隐藏
						mesh.isVisible = false;
					resolve();
				}
			);
		});
	}
	
	//进入动画
	intoAnimation(time){
		return new Promise((resolve, reject)=>{
			/*//由透明变不透明
			const alphaAnimation = new BABYLON.Animation(
				"animation_alpha", //名称
				"material.alpha", //属性
				30, //FPS
				BABYLON.Animation.ANIMATIONTYPE_FLOAT, //属性类型
				BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT //仅一次
			);
			alphaAnimation.setKeys([ //关键帧
				{ frame: 0, value: 0 },
				{ frame: 100, value: 1 }
			]);
			alphaAnimation.setEasingFunction( this.easingFunction ); //缓动函数*/
			
			for (const line of this.lines.border)
				this.sizeInAnimation(line, 100); //由小到大动画
			
			for (let i=this.columns-1; i>=0; i--)
				for (let j=this.rows-1; j>=0; j--)
					for (let k=this.depth-1; k>=0; k--)
						for (let l=this.width-1; l>=0; l--){
							const mesh = this.pieces[i][j][k][l];
							if (!mesh) continue; //无棋子
							if (!OR(mesh.goalVisible, mesh.isVisible)) continue; //已隐藏
							
							//位置进入动画
							BABYLON.Animation.CreateAndStartAnimation(
								"animation_comeIn", //名称
								mesh, //物体
								"position", //属性
								30, //fps
								100, //frames
								new BABYLON.Vector3(0, 0, 0), //from
								new BABYLON.Vector3(...this.ijk2xyz(i, j, k)), //to
								BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT, //mode
								this.easingFunction, //缓动函数
								resolve
							);
							
							//由小到大动画
							this.sizeInAnimation(mesh, 100);
						}
		});
	}
	
	
	/* 坐标 */
	//棋子序号 转 场景坐标
	ijk2xyz(i, j, k){
		return [
			(i - (this.columns-1)/2 ) * this.spacing.x,
			(j - (this.rows-1)/2 ) * this.spacing.y,
			(k - (this.depth - 1)/2 ) * this.spacing.z
		];
	}
	//场景坐标 转 棋子序号
	xyz2ijk(x, y, z){
		return [
			Math.round( x/this.spacing.x + (this.columns - 1)/2 ),
			Math.round( y/this.spacing.y + (this.rows - 1)/2 ),
			Math.round( z/this.spacing.z + (this.depth - 1)/2 )
		];
	}
	
	
	/* 棋子 */
	//添加棋子
	add(i, j, k, l, type){
		const piece = this.templates[type].clone(`pieces(${i},${j},${k},${l})`);
		piece.isVisible = true; //显示
		piece.position.set( ...this.ijk2xyz(i, j, k) );
		piece.freezeWorldMatrix();
		//piece.cullingStrategy = BABYLON.AbstractMesh.CULLINGSTRATEGY_BOUNDINGSPHERE_ONLY; //网格剔除策略：边界球
		this.pieces[i][j][k][l] = piece;
		if (type == "none" || type == "mistake")
			this.places[i][k] = piece;
		return this;
	}
	
	//隐藏棋子
	visible(i, j, k, l, visible){
		const mesh = this.pieces[i][j][k][l];
		mesh.goalVisible = mesh.isVisible = visible;
		return this;
	}
	
	//删除棋子
	remove(i, j, k, l){
		if (this.pieces[i][j][k][l])
			this.pieces[i][j][k][l].dispose();
		this.pieces[i][j][k][l] = null;
		return this;
	}
	
	//为占位符或防误触
	isBlock(mesh){
		if ( mesh.material == this.materials.none ||
			mesh.material == this.materials.mistake
		) return true;
		return false;
	}
	
	//设置棋子
	set(i, j, k, l, type){
		const mesh = this.pieces[i][j][k][l];
		switch (type){
			/*case null: //隐藏
				if (mesh) //有棋子
					this.remove(i, j, k, l);
				break;*/
				
			case "none": //占位符
				if (mesh){ //有棋子
					//this.visible(i, j, k, l, true); //显示
					if (mesh.material == this.materials.none)
						return; //类型相同不改
					if (mesh.material == this.materials.mistake) //同形状 改材质
						return (mesh.material = this.materials.none);
					this.pieces[i][j][k][l] = null; //取消引用
					this.sizeOutAnimation(mesh, 20)
						.then(()=>{
							if (!mesh.goalVisible) //仍要隐藏
								mesh.dispose();
						}); //否则删除原有棋子
					//this.remove(i, j, k, l);
				}
				if (this.inRange(i, j, k, l)){
					this.add(i, j, k, l, "none"); //添加棋子
					this.sizeInAnimation(this.pieces[i][j][k][l], 20);
				}
				break;
				
			/*case "mistake": //防误触
				this.places[i][k].material = this.materials.mistake;
				break;*/
				
			case "black": //黑棋
				if (mesh){ //有棋子
					//this.visible(i, j, k, l, true); //显示
					if (mesh.material == this.materials.black)
						return this.visible(i, j, k, l, true); //类型相同不改
					if (mesh.material == this.materials.white) //同形状 改材质
						return (mesh.material = this.materials.black);
					if (mesh.material == this.materials.mistake)
						mesh.material = this.materials.none; //关闭防误触
					this.sizeOutAnimation(mesh, 20); //隐藏占位符
					//this.visible(i, j, k, l, false); //隐藏占位符
				}
				this.add(i, j, k, l, "black"); //添加棋子
				if (l == this.widthRange){ //同一层
					this.sizeInAnimation(this.pieces[i][j][k][l]); //显示
				}else{
					this.visible(i, j, k, l, false);
				}
				break;
				
			case "white": //白棋
				if (mesh){ //有棋子
					//this.visible(i, j, k, l, true); //显示
					if (mesh.material == this.materials.white)
						return this.visible(i, j, k, l, true); //类型相同不改
					if (mesh.material == this.materials.black) //同形状 改材质
						return (mesh.material = this.materials.white);
					if (mesh.material == this.materials.mistake)
						mesh.material = this.materials.none; //关闭防误触
					this.sizeOutAnimation(mesh, 20); //隐藏占位符
					//this.visible(i, j, k, l, false); //隐藏占位符
				}
				this.add(i, j, k, l, "white"); //添加棋子
				if (l == this.widthRange){
					this.sizeInAnimation(this.pieces[i][j][k][l]); //显示
				}else{
					this.visible(i, j, k, l, false);
				}
				break;
				
		}
	}
	
	//清除棋子
	clear(){
		for (let i=this.columns-1; i>=0; i--)
			for (let j=this.rows-1; j>=0; j--)
				for (let k=this.depth-1; k>=0; k--)
					for (let l=this.width-1; l>=0; l--)
						this.set(i, j, k, l, "none");
		return this;
	}
	
	
	/* 划线 */
	//添加划线
	addLine([i0, j0, k0, l0], [i1, j1, k1, l1], width=1, material){
		const type = material=="winLines"? "win": "analyse";
		this.lines.update[type].push({
			start: [i0, j0, k0, l0],
			end: [i1, j1, k1, l1],
			width,
			material
		});
		console.log("addLine", type, this.lines.update[type])
		
		if ( this.widthRange < Math.min(l0, l1) ||
			this.widthRange > Math.max(l0, l1)
		) return; //不在同层不显示
		
		let from, to;
		if ( this.widthRange == l0 &&
			this.widthRange == l1
		){
			from = this.ijk2xyz(i0, j0, k0),
			to = this.ijk2xyz(i1, j1, k1);
		}else{
			const range = [
					Math.limitRange( this.widthRange-0.5, Math.min(l0, l1), Math.max(l0, l1) ),
					Math.limitRange( this.widthRange+0.5, Math.min(l0, l1), Math.max(l0, l1) )
				],
				di = i1 - i0,
				dj = j1 - j0,
				dk = k1 - k0,
				dl = l1 - l0,
				drange = [range[0]-l0, range[1]-l0];
			from = this.ijk2xyz(
				+i0 + di * drange[0]/dl,
				+j0 + dj * drange[0]/dl,
				+k0 + dk * drange[0]/dl
			),
			to = this.ijk2xyz(
				+i0 + di * drange[1]/dl,
				+j0 + dj * drange[1]/dl,
				+k0 + dk * drange[1]/dl
			);
		}
		
		const [dx, dy, dz] = to.map((v,i) => v-from[i]),
			len = Math.sqrt(dx*dx + dy*dy + dz*dz),
			line = BABYLON.MeshBuilder.CreateCylinder(
				"cylinder_line",
				{
					height: len,
					diameter: width, //直径
					tessellation: 8, //细分度
					heightSubdivs: 1 //高度细分度
				},
				this.scene
			);
		line.position = new BABYLON.Vector3(
			...to.map( (v,i) => (v+from[i])/2 ) //取平均值
		);
		const symbolX = dx>=0? +1: -1; //dx的符号
		line.addRotation(0, 0, -Math.PI/2)
			.addRotation(Math.atan(dz && dz/dx), 0, 0)
			.addRotation(0, 0, Math.atan(dy / (symbolX * Math.sqrt(dx*dx + dz*dz)) ));
		
		if (material)
			line.material = this.materials[material];
		
		line.isPickable = false; //禁止点击
		
		this.lines[type].push(line);
		return line;
	}
	
	//添加球形划线
	addLinesCircle(pos, radius, color, type){
		console.log("addLinesCircle", pos, radius, color, type)
		
		let pass = false;
		for (const v of this.lines.update[type])
			if ( v.pos &&
				pos.every((p,i) => p==v.pos[i])
			){ //有相同 跳过
				pass = true;
				break;
			}
		if (pass) return this; //不添加
		
		if (pos[3] != this.widthRange){ //w不同
			board.lines.update[type].push({ pos, radius, color });
			return this;
		}
		
		board.lines.update[type].push({ pos, radius, color });
		
		const lines = this.addCircle(pos, radius, color);
		lines.forEach(line => this.sizeInAnimation(line)); //淡入
		this.lines[type].push(...lines);
		return this;
	}
	
	//更新划线
	updateLines(type){
		if (type){
			this.removeLines(...this.lines[type]); //先删除旧线
			this.lines[type] = [];
			const lines = this.lines.update[type]; //先记录
			this.lines.update[type] = []; //再清空
			for (const line of lines)
				if (line.pos){ //球形划线
					this.addLinesCircle(line.pos, line.radius, line.color, type); //再重新添加
				}else{ //普通划线
					this.addLine(line.start, line.end, line.width, line.material); //再重新添加
				}
		}else{
			this.updateLines("win");
			this.updateLines("analyse");
		}
	}
	
	//添加圆圈标记
	addCircle([i, j, k, l], radius, color, delta=30){
		const pos = new BABYLON.Vector3(...this.ijk2xyz(i, j, k));
		console.log("addCircle", [i, j, k, l], pos)
		
		if (color)
			color = new BABYLON.Color4(...color, 0.6); //透明度0.6
		const paths = Array.new(()=>[], 9),
			colors = color? []: null;
		for (let theta=0; theta<=360; theta+=delta){
			//x
			paths[0].push( new BABYLON.Vector3(
				radius * Math.sin(-Math.PI/6), // -30°
				radius * Math.cos(-Math.PI/6) * Math.cos(theta/180*Math.PI),
				radius * Math.cos(-Math.PI/6) * Math.sin(theta/180*Math.PI)
			) );
			paths[1].push( new BABYLON.Vector3(
				0, // 0°
				radius * Math.cos(theta/180*Math.PI),
				radius * Math.sin(theta/180*Math.PI)
			) );
			paths[2].push( new BABYLON.Vector3(
				radius * Math.sin(Math.PI/6), // 30°
				radius * Math.cos(Math.PI/6) * Math.cos(theta/180*Math.PI),
				radius * Math.cos(Math.PI/6) * Math.sin(theta/180*Math.PI)
			) );
			
			//y
			paths[3].push( new BABYLON.Vector3(
				radius * Math.cos(-Math.PI/6) * Math.cos(theta/180*Math.PI),
				radius * Math.sin(-Math.PI/6), // -30°
				radius * Math.cos(-Math.PI/6) * Math.sin(theta/180*Math.PI)
			) );
			paths[4].push( new BABYLON.Vector3(
				radius * Math.cos(theta/180*Math.PI),
				0, // 0°
				radius * Math.sin(theta/180*Math.PI)
			) );
			paths[5].push( new BABYLON.Vector3(
				radius * Math.cos(Math.PI/6) * Math.cos(theta/180*Math.PI),
				radius * Math.sin(Math.PI/6), // 30°
				radius * Math.cos(Math.PI/6) * Math.sin(theta/180*Math.PI)
			) );
			
			//z
			paths[6].push( new BABYLON.Vector3(
				radius * Math.cos(-Math.PI/6) * Math.cos(theta/180*Math.PI),
				radius * Math.cos(-Math.PI/6) * Math.sin(theta/180*Math.PI),
				radius * Math.sin(-Math.PI/6) // -30°
			) );
			paths[7].push( new BABYLON.Vector3(
				radius * Math.cos(theta/180*Math.PI),
				radius * Math.sin(theta/180*Math.PI),
				0 // 0°
			) );
			paths[8].push( new BABYLON.Vector3(
				radius * Math.cos(Math.PI/6) * Math.cos(theta/180*Math.PI),
				radius * Math.cos(Math.PI/6) * Math.sin(theta/180*Math.PI),
				radius * Math.sin(Math.PI/6) // 30°
			) );
			
			if (color)
				colors.push(color);
		}
		
		const lines = [];
		for (const path of paths){
			const line = BABYLON.MeshBuilder.CreateLines("lines", {points: path, colors}, this.scene);
			line.position = pos;
			line.isPickable = false; //禁止点击
			lines.push(line);
		}
		
		return lines;
	}
	
	//添加或移动圆圈标记
	moveCircle([i, j, k, l], radius, color, delta=30){
		console.log("moveCircle", [i, j, k, l])
		
		const e = $("#axis > .scale > li");
		for (let index=0; index<e.length; index++)
			$(e[index]).removeClass("play").addClass(index==l? "play": "");
		
		this.lines.update.circle = [i, j, k, l]; //存储位置
		if (this.lines.circle.length > 0){ //已有
			const pos = new BABYLON.Vector3(...this.ijk2xyz(i, j, k));
			for (const line of this.lines.circle){
				line.position = pos;
				if (l == this.widthRange){ //在同一三维层
					this.sizeInAnimation(line, 20);
				}else{
					line.goalVisible = line.isVisible = false;
				}
			}
			
		}else{ //未创建
			const lines = this.addCircle([i, j, k, l], radius, color, delta);
			if (l == this.widthRange){ //在同一三维层
				lines.forEach( line => this.sizeInAnimation(line) );
			}else{
				lines.forEach( line => line.goalVisible=line.isVisible=false );
			}
			this.lines.circle.push(...lines);
		}
		
		return this;
	}
	
	//更新圆圈标记
	updateCircle(){
		if (this.lines.update.circle.length > 0)
			this.moveCircle(this.lines.update.circle);
	}
	
	//隐藏划线
	visibleLines(visible, type){
		if (type){
			this.lines[type].forEach(v => v.isVisible=visible);
		}else{
			this.lines.win.forEach(v => v.isVisible=visible);
			this.lines.analyse.forEach(v => v.isVisible=visible);
		}
		return this;
	}
	
	//删除划线
	removeLines(...lines){
		for (const line of lines)
			line.dispose();
		return this;
	}
	
	//清空划线
	clearLines(type){
		if (type){
			this.removeLines(...this.lines[type]); //删除所有划线
			this.lines[type] = [];
			if (this.lines.update[type])
				this.lines.update[type] = [];
		}else{
			this.removeLines(...this.lines.win, ...this.lines.analyse, ...this.lines.circle); //删除所有划线
			this.lines.win = [];
			this.lines.analyse = [];
			this.lines.circle = [];
			this.lines.update.win = [];
			this.lines.update.analyse = [];
			this.lines.update.circle = [];
		}
		return this;
	}
	
	
	/* 占位符控制 */
	//层增大
	layerAdd(){
		if (this.columnRange[0] <= 0) return this;
		if (this.columnRange[1] >= this.columns-1) return this;
		if (this.depthRange[0] <= 0) return this;
		if (this.depthRange[1] >= this.depth-1) return this;
		
		this.columnRange[0]--;
		this.columnRange[1]++;
		this.depthRange[0]--;
		this.depthRange[1]++;
		
		return this.updateLayer(+1);
	}
	
	//层减小
	layerSub(){
		if (this.columnRange[0] == this.columnRange[1]) return this;
		if (this.columnRange[0]-1 == this.columnRange[1]) return this;
		if (this.depthRange[0] == this.depthRange[1]) return this;
		if (this.depthRange[0]-1 == this.depthRange[1]) return this;
		
		this.columnRange[0]++;
		this.columnRange[1]--;
		this.depthRange[0]++;
		this.depthRange[1]--;
		
		return this.updateLayer(-1);
	}
	
	//高度增加
	heightAdd(){
		if (this.rowRange >= this.rows-1) return this;
		this.rowRange++;
		
		return this.updateHeight(this.rowRange-1);
	}
	
	//高度减小
	heightSub(){
		if (this.rowRange <= 0) return this;
		this.rowRange--;
		
		return this.updateHeight(this.rowRange+1);
	}
	
	//四维厚度增加
	widthAdd(){
		if (this.widthRange >= this.width-1) return this;
		this.widthRange++;
		
		return this.updateWidth(this.widthRange-1);
	}
	
	//四维厚度减小
	widthSub(){
		if (this.widthRange <= 0) return this;
		
		this.widthRange--;
		
		return this.updateWidth(this.widthRange+1);
	}
	
	//设置四维厚度
	setWidth(width){
		const before = this.widthRange;
		this.widthRange = Math.limitRange(width, 0, this.width-1);
		if (this.widthRange != before) //改变
			this.updateWidth(before);
		return this;
	}
	
	
	/* 占位符更新 */
	//可显示（在范围内）
	inRange(i, j, k, l){
		if ( j == this.rowRange &&
			l == this.widthRange &&
			i >= this.columnRange[0] && i <= this.columnRange[1] &&
			k >= this.depthRange[0] && k <= this.depthRange[1]
		) return true; //在加载范围内
		return false;
	}
	
	//更新层
	updateLayer(type){
		switch (type){
			case +1: //增加
				for (let i=this.columns-1; i>=0; i--)
					for (let k=this.depth-1; k>=0; k--)
						if ( i >= this.columnRange[0] && i <= this.columnRange[1] &&
							k >= this.depthRange[0] && k <= this.depthRange[1] &&
							( i == this.columnRange[0] || i == this.columnRange[1] ||
								k == this.depthRange[0] || k == this.depthRange[1] ) &&
							( !this.pieces[i][this.rowRange][k][this.widthRange] ||
								this.isBlock(this.pieces[i][this.rowRange][k][this.widthRange]) )
						){ //在范围内 且 无棋子
							const mesh = this.places[i][k];
							//mesh.material = this.materials.none; //取消防误触
							this.sizeInAnimation(mesh, 20); //显示
						}
				break;
				
			case -1: //去除
				const columnBefore = [
						this.columnRange[0]+type,
						this.columnRange[1]-type
					],
					depthBefore = [
						this.depthRange[0]+type,
						this.depthRange[1]-type
					];
				
				for (let i=this.columns-1; i>=0; i--)
					for (let k=this.depth-1; k>=0; k--)
						if ( i >= columnBefore[0] && i <= columnBefore[1] &&
							k >= depthBefore[0] && k <= depthBefore[1] &&
							( i == columnBefore[0] || i == columnBefore[1] ||
								k == depthBefore[0] || k == depthBefore[1] )
						){ //在范围内
							const mesh = this.places[i][k];
							//mesh.material = this.materials.none; //取消防误触
							this.sizeOutAnimation(mesh, 20); //隐藏
						}
				break;
		}
		
		return this;
	}
	
	//更新高度
	updateHeight(before){
		const y = this.ijk2xyz(0, this.rowRange, 0)[1];
		for (let i=this.columns-1; i>=0; i--)
			for (let k=this.depth-1; k>=0; k--){
				const mesh = this.places[i][k];
				
				//取消防误触
				mesh.material = this.materials.none;
				
				//mesh.enableBlending = true; //混合动画
				//高度改变动画
				BABYLON.Animation.CreateAndStartAnimation(
					"animation_height",
					mesh,
					"position.y",
					30,
					20,
					mesh.position.y,
					y,
					BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT,
					this.easingFunction
				);
				
				//隐藏与显示动画
				if (this.inRange(i, this.rowRange, k, this.widthRange)) //在范围内
					if (OR(mesh.goalVisible, mesh.isVisible) && this.pieces[i][this.rowRange][k][this.widthRange]){ //已显示 且 有棋子
						this.sizeOutAnimation(mesh, 20); //隐藏
					}else if (!OR(mesh.goalVisible, mesh.isVisible) && !this.pieces[i][this.rowRange][k][this.widthRange]){ //未显示 且 无棋子
						this.sizeInAnimation(mesh, 20); //显示
					}
				
				//修改引用
				if ( this.isBlock(this.pieces[i][before][k][this.widthRange]) ) //是占位符或防误触
					this.pieces[i][before][k][this.widthRange] = null; //取消引用
				if ( !this.pieces[i][this.rowRange][k][this.widthRange] ) //没有引用
					this.pieces[i][this.rowRange][k][this.widthRange] = mesh; //引用占位符
			}
		
		return this;
	}
	
	//更新四维厚度
	updateWidth(before){
		const e = $("#axis > .scale > li");
		for (let index=0; index<e.length; index++)
			$(e[index]).removeClass("in").addClass(index==this.widthRange? "in": "");
		
		//更新划线
		this.updateLines();
		this.updateCircle();
		
		for (let i=this.columns-1; i>=0; i--)
			for (let k=this.depth-1; k>=0; k--){
				for (let j=this.rows-1; j>=0; j--){
					const last = this.pieces[i][j][k][before],
						mesh = this.pieces[i][j][k][this.widthRange];
					
					if (last && last.material == this.materials.mistake)
						last.material = this.materials.none;
					
					//棋子隐藏
					if ( last && !this.isBlock(last) ) //是棋子
						this.sizeOutAnimation(last, 20); //隐藏上次的
					if ( mesh && !this.isBlock(mesh) ) //是棋子
						this.sizeInAnimation(mesh, 20); //显示这次的
				}
				
				const place = this.places[i][k];
				//隐藏与显示动画
				if (this.inRange(i, this.rowRange, k, this.widthRange)) //在范围内
					if (OR(place.goalVisible, place.isVisible) && this.pieces[i][this.rowRange][k][this.widthRange]){ //已显示 且 有棋子
						this.sizeOutAnimation(place, 20); //隐藏
					}else if (!OR(place.goalVisible, place.isVisible) && !this.pieces[i][this.rowRange][k][this.widthRange]){ //未显示 且 无棋子
						this.sizeInAnimation(place, 20); //显示
					}
				
				//修改引用
				if ( this.isBlock(this.pieces[i][this.rowRange][k][before]) ) //是占位符或防误触
					this.pieces[i][this.rowRange][k][before] = null; //取消引用
				if ( !this.pieces[i][this.rowRange][k][this.widthRange] ) //没有引用
					this.pieces[i][this.rowRange][k][this.widthRange] = place; //引用占位符
			}
	}
}


const N = +location.getQueryString("N"),
	{cols, rows, depth, width} = JSON.parse(location.getQueryString("size")),
	{mistake=true, skybox=true, moon=true, border=true} = JSON.parse(localStorage.getItem("五子棋_个性化设置") || "{}");
const board = new Board({ //棋盘
	canvas: $("#board")[0],
	background: [0xff/0xff, 0xfa/0xff, 0xe8/0xff],
	rows,
	columns: cols,
	depth,
	width,
	none: {
		size: 6,
		color: [
			[0.9, 0.9, 0.9], //漫反射
			[1, 1, 1] //镜面反射
		]
	},
	mistake: {
		size: 6,
		color: [
			[0, 0, 1], //漫反射
			[0.6, 0.6, 1] //镜面反射
		]
	},
	black: {
		size: 8,
		color: [
			[0, 0, 0], //漫反射
			[0.8, 0.8, 0.8] //镜面反射
		]
	},
	white: {
		size: 8,
		color: [
			[1, 1, 1], //漫反射
			[1, 1, 1] //镜面反射
		]
	},
	winLines: {
		color: [
			[1, 0, 0], //漫反射
			[1, 0.8, 0.8], //镜面反射
			[0.2, 0.2, 0.2] //自发光颜色
		]
	},
	spacing: {x: 10, y: 10, z: 10}
});
let waiting = false, //未等待
	gameOverMusic; //游戏结束音乐

const loadingId = layer.load(2);
/*$("#axis > .scale").empty();
for (let i=0; i<width; i++){
	$("#axis > .scale").append(
		$("<li></li>")
			.addClass(i == board.widthRange? "in": "")
			.click(()=>board.setWidth(i))
	);
}*/
setTimeout(()=>{
	board.loadScene(skybox, moon, border);
	layer.close(loadingId);
	//init();
}, 600);


let socket, //socket.io
	send, //send
	ID, //分配的ID
	oldID = location.getQueryString("id"), //上次分配的ID
	ROOM = location.getQueryString("room"), //房间
	TYPE = !!+location.getQueryString("type"), //房主: true, 成员: false
	FIRST = JSON.parse( location.getQueryString("first") ), //先手
	lastMistake = null; //上次防误触物体
$("body > header > h1").html(ROOM);


let game = {
		turn: 1, //下棋方 1:黑, 0:白
		startTime: null, //开始游戏时间
		steps: 0, //下棋总步数
		winner: false, //获胜者 1:黑, 0:白, null:平 false:未获胜
		t0: [null, null], //上次时间
		ids: [null, null], //计时器id
		time: [
			{total: 900, step: 60}, //0: 白
			{total: 900, step: 60} //1: 黑
		], //剩余时间
		timeWarning: { //时间警告
			total: {
				60: ["#f00", 1.5],
				300: ["#fa0", 1.2],
				900: ["inherit", 1]
			},
			step: {
				10: ["#f00", 1.5],
				30: ["#fa0", 1.2],
				60: ["inherit", 1]
			}
		},
		scores: {
			now: [0, 0], //当前
			goal: [0, 0], //目标
			t0: +new Date(),
			k: 0.005
		}, //分数 白:0 黑:1
		lines: [], //分析连线
		linesDisplay: false, //显示连线
		
		//更新时间显示
		updateTime(turn){
			let total, step,
				totalElem, stepElem;
			
			//黑
			total = Math.max(this.time[1].total, 0);
			step = Math.max(this.time[1].step, 0);
			
			totalElem = $("#main > .black > div > .time > .total"),
			stepElem = totalElem.nextAll(".step");
			
			totalElem.html(
				( ~~(total/60) +"").padStart(2,"0") + ":" + ( ~~total%60 +"").padStart(2, "0")
			);
			for (const [i,v] of Object.entries(this.timeWarning.total))
				if (total <= i){
					totalElem.css("color", v[0])
						.css("font-size", v[1] + "em");
					break;
				}
			
			stepElem.html(
				~~step + "s"
			);
			for (const [i,v] of Object.entries(this.timeWarning.step))
				if (step <= i){
					stepElem.css("color", v[0])
						.css("font-size", v[1] + "em");
					break;
				}
			
			if (~~step < 10 && ~~step >= 9)
				this.flash(this.turn, "#f00"); //背景闪烁
			
			//白
			total = Math.max(this.time[0].total, 0);
			step = Math.max(this.time[0].step, 0);
			
			totalElem = $("#main > .white > div > .time > .total"),
			stepElem = totalElem.nextAll(".step");
			
			totalElem.html(
				( ~~(total/60) +"").padStart(2,"0") + ":" + ( ~~total%60 +"").padStart(2, "0")
			);
			for (const [i,v] of Object.entries(this.timeWarning.total))
				if (total <= i){
					totalElem.css("color", v[0])
						.css("font-size", v[1] + "em");
					break;
				}
			
			stepElem.html(
				~~step + "s"
			);
			for (const [i,v] of Object.entries(this.timeWarning.step))
				if (step <= i){
					stepElem.css("color", v[0])
						.css("font-size", v[1] + "em");
					break;
				}
			
			if (~~step < 10 && ~~step >= 9)
				this.flash(this.turn, "#f00"); //背景闪烁
		},
		
		//背景闪烁
		flash(turn, color){
			let elem;
			if (this.turn == 1){ //黑
				elem = $("#main > div.black > i");
			}else{ //白
				elem = $("#main > div.white > i");
			}
			elem.css("background-color", color);
			setTimeout(()=> elem.css("background-color", ""), 150);
			setTimeout(()=> elem.css("background-color", color), 300);
			setTimeout(()=> elem.css("background-color", ""), 450);
		},
		
		//切换下棋方
		set(turn){
			if (this.time[this.turn].total <= 0){ //超出局时
				this.time[this.turn].step += 5;
			}else{
				this.time[this.turn].step = 60; //重置步时
				//this.time[this.turn].total += 5; //增加局时
			}
			this.updateTime(this.turn);
			clearInterval( this.ids[this.turn] );
			
			this.turn = turn;
			this.flash(this.turn, "#af8"); //背景闪烁
			this.t0[this.turn] = +new Date();
			this.ids[this.turn] = setInterval(()=>{
				const interval = (new Date() - this.t0[this.turn]) / 1000;
				this.t0[this.turn] = +new Date();
				
				this.time[this.turn].total -= interval;
				this.time[this.turn].step -= interval;
				this.updateTime(this.turn);
				
				if (this.time[this.turn].step <= 0){ //超出步时
					if (game.winner !== false) return; //游戏已结束
					game.over(this.turn!=this.first, this.turn==1?"黑方超时":"白方超时");
				}
				
			}, 200);
		},
		
		//下棋
		play(i, j, k, l, mistake){
			console.log("play", {i, j, k, l}, mistake)
			
			request({
				type: "play", //下棋
				i, j, k, l,
				direct: mistake===false? true: false
			}).then((data)=>{
				waiting = false;
				console.log("play callback:", data.action, {i, j, k, l})
				switch (data.action){
					case 2: //下黑棋
						board.set(i, j, k, l, "black");
						board.moveCircle([i, j, k, l], 5, [0,1,0]);
						game.set(0);
						game.steps++;
						send("gobang_game_play", {
							room: ROOM,
							pos: {i, j, k, l},
							side: 2, //黑棋
							time: +new Date(),
							totalTime: this.time[+this.first].total
						});
						break;
						
					case 1: //下白棋
						board.set(i, j, k, l, "white");
						board.moveCircle([i, j, k, l], 5, [0,1,0]);
						game.set(1);
						game.steps++;
						send("gobang_game_play", {
							room: ROOM,
							pos: {i, j, k, l},
							side: 1, //白棋
							time: +new Date(),
							totalTime: this.time[+this.first].total
						});
						break;
						
					case -2: //添加标记
						mistake.material = board.materials.mistake;
						lastMistake = mistake;
						/*lastMistake.splice(0, 3);
						lastMistake.push(i, j, k, l);
						board.set(i, j, k, l, "mistake");*/
						break;
						
					case -1: //删除标记
						lastMistake.material = board.materials.none;
						/*const [x, y, z, w] = lastMistake;
						board.set(x, y, z, w, "none");*/
						break;
				}
			});
		},
		
		//游戏结束
		over(win, text){
			console.log("game over", win, text)
			
			const t = (new Date() - this.startTime) / 1000;
			clearInterval( this.ids[this.turn] );
			$("#toolBtn > .review").removeAttr("disabled"); //解除禁用
			
			if (win === true){
				this.winner = +this.first;
				layer.alert(`<span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>（</span>${game.steps/2}<span>回合</span><span>）<span>`,
					{title: "胜利！", icon: 6}
				);
				$("#state").html("胜利！");
				output(`<span>胜利：</span><span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>（</span>${game.steps/2}<span>回合</span><span>）</span>`, true);
				
				if (gameOverMusic)
					gameOverMusic.stop();
				gameOverMusic = new Player("./music/好运来.mp3").play();
				
			}else if (win === false){
				this.winner = 1-this.first;
				layer.alert(`<span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>（</span>${game.steps/2}<span>回合</span><span>）<span>`,
					{title: "你失败了！", icon: 5}
				);
				$("#state").html("你失败了！");
				output(`<span>你失败了：</span><span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>（</span>${game.steps/2}<span>回合</span><span>）</span>`, true);
				
				if (gameOverMusic)
					gameOverMusic.stop();
				gameOverMusic = new Player("./music/凉凉.mp3").play();
				
			}else{
				this.winner = null;
				layer.alert(`<span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>（</span>${game.steps/2}<span>回合</span><span>）<span>`,
					{title: "游戏结束"}
				);
				$("#state").html("游戏结束");
				output(`<span>游戏结束：</span><span>${text}</span><br><span>历时</span>${~~(t/60)}<span>分</span>${~~t%60}<span>秒</span><br>${game.steps}<span>步</span><span>（</span>${game.steps/2}<span>回合</span><span>）</span>`, true);
				
			}
		},
		
		//更新分数
		updateScores(){
			const t = new Date()-this.scores.t0,
				now = this.scores.now,
				goal = this.scores.goal,
				k = this.scores.k,
				e = $("#tools > .scores");
			this.scores.t0 = +new Date();
			if (t > 300) return; //跳帧
			
			now[0] += (goal[0] - now[0]) *t *k;
			now[1] += (goal[1] - now[1]) *t *k;
			
			const black = now[1] + 0.001,
				white = now[0] + 0.001,
				total = black + white,
				ctx = e[0].getContext("2d"),
				width = e.attr("width"),
				height = e.attr("height");
			
			//清除背景
			ctx.beginPath();
			ctx.fillStyle = "#fff";
			ctx.fillRect(0, 0, width, height);
			
			//背景
			ctx.beginPath();
			ctx.fillStyle = "#000";
			ctx.moveTo(0, 0);
			ctx.lineTo(black / total * width + 5, 0);
			ctx.lineTo(black / total * width - 5, height);
			ctx.lineTo(0, height);
			ctx.fill();
			
			ctx.beginPath();
			ctx.fillStyle = "#fff";
			ctx.moveTo(width, 0);
			ctx.lineTo(black / total * width + 5, 0);
			ctx.lineTo(black / total * width - 5, height);
			ctx.lineTo(width, height);
			ctx.fill();
			
			//文字
			ctx.beginPath();
			ctx.fillStyle = "#fff"; //白字
			ctx.textAlign = "left";
			ctx.textBaseline = "top"; //基线：左上
			ctx.font = height*5/6 + "px xs"; // 5/6vh
			ctx.shadowOffsetX = 1;
			ctx.shadowOffsetY = 1;
			ctx.shadowColor = "#000";
			ctx.shadowBlur = 3;
			ctx.fillText(Math.round(black-0.001, 2), height/6, height/12); //margin: 0.2vh ;0.1vh
			
			ctx.beginPath();
			ctx.fillStyle = "#000"; //黑字
			ctx.textAlign = "right";
			ctx.textBaseline = "top"; //基线：右上
			ctx.font = height*5/6 + "px xs"; // 5/6vh
			ctx.shadowOffsetX = 1;
			ctx.shadowOffsetY = 1;
			ctx.shadowColor = "#fff";
			ctx.shadowBlur = 3;
			ctx.fillText(Math.round(white-0.001, 2), width-height/6, height/12); //margin: 0.2vh ;0.1vh
		},
		
		//更新分析画线
		updateLines(display=true){
			board.clearLines("analyse");
			
			if (display) //需显示
				for (const line of this.lines.sort((a,b)=>a.width<b.width) ){ //width降序
					if (line.width < 3) continue; //连子>=3 避免过密
					if ( line.start[3] != line.end[3] &&
						line.start[0] == line.end[0] &&
						line.start[1] == line.end[1] &&
						line.start[2] == line.end[2]
					){ //仅w不同
						const min = Math.min(line.start[3], line.end[3]),
							max = Math.max(line.start[3], line.end[3]);
						for (let l=min; l<=max; l++)
							board.addLinesCircle(
								[line.start[0], line.start[1], line.start[2], l],
								4 + (line.width-3+1)/5,
								line.pic==2? [1,1,1]: [0,0,0],
								"analyse"
							);
					}else{
						board.addLine(
							line.start,
							line.end,
							(line.width-3+1)/2,
							line.pic==2? "white": "black"
						);
					}
				}
			
			game.linesDisplay = display;
		}
	},
	worker,
	workerCallback = {  //收到消息回调
		//电脑下
		computer(data){
			const {pos, value} = data;
			if (game.winner !== false) return; //游戏已结束
			if (game.first){ //电脑白棋
				board.set(...pos, "white");
				board.moveCircle(pos, 5, [0,1,0]);
				game.set(1);
			}else{ //电脑黑棋
				board.set(...pos, "black");
				board.moveCircle(pos, 5, [0,1,0]);
				game.set(0);
			}
		},
	
		//获胜
		won(data){
			const {winner, steps} = data;
			
			if (game.winner !== false) return; //游戏已结束
			game.over(winner-1==game.first, winner==2?"黑方胜":"白方胜");
		},
		
		//获胜画线
		lines(data){
			const {lines} = data;
			
			/*lines.forEach( v => {
				[v.start[0], v.start[1], v.start[2]] = board.ijk2xyz(...v.start);
				[v.end[0], v.end[1], v.end[2]] = board.ijk2xyz(...v.end);
			});*/
			
			board.clearLines("win");
			for (const line of lines.sort((a,b)=>a.width<b.width) ) //width降序
				if ( line.start[3] != line.end[3] &&
					line.start[0] == line.end[0] &&
					line.start[1] == line.end[1] &&
					line.start[2] == line.end[2]
				){ //仅w不同
					const min = Math.min(line.start[3], line.end[3]),
						max = Math.max(line.start[3], line.end[3]);
					for (let l=min; l<=max; l++)
						board.addLinesCircle(
							[line.start[0], line.start[1], line.start[2], l],
							4 + (N-3+1)/5 + 0.1,
							[1,0,0],
							"win"
						);
				}else{
					board.addLine(
						line.start,
						line.end,
						(0.1+N-3+1)/2,
						"winLines"
					);
				}
			
			for (const line of lines){
				const start = Math.min(line.start[3], line.end[3]),
					end = Math.max(line.start[3], line.end[3]);
				for (let index=0; index<board.width; index++)
					if (index >= start && index <= end)
						$("#axis > .scale > li:eq("+ index +")").addClass("win");
			}
			
			//worker.terminate(); //关闭线程
		},
		
		//平局
		gameOver(){
			if (game.winner !== false) return; //游戏已结束
			game.over(null, "平局");
		},
		
		//分析
		scores(data){
			console.table(data.scores);
			
			const {scores, lines} = data;
			/*lines.forEach( v => {
				[v.start[0], v.start[1], v.start[2]] = board.ijk2xyz(...v.start);
				[v.end[0], v.end[1], v.end[2]] = board.ijk2xyz(...v.end);
			});*/
			
			game.scores.goal = scores;
			game.lines = lines;
			
			if (game.linesDisplay){ //已显示
				game.updateLines(true);
			}else{
				game.updateLines(false);
			}
		}
	},
	scores_id = null, //分数更新id
	review_id = null; //复盘id



//初始化
function init(){
	//w轴
	$("#axis > .scale").empty();
	for (let i=0; i<board.width; i++)
		$("#axis > .scale").append(
			$("<li></li>")
				.addClass(i == board.widthRange? "in": "")
				.click(()=>board.setWidth(i))
		);
	
	//游戏结束音乐
	if (gameOverMusic)
		gameOverMusic.stop();
	
	//棋盘
	board.clearLines(); //删除所有划线
	waiting = true; //禁止点击
	board.clear().clearLines().intoAnimation().then(()=>{
		// game.set(1); //黑棋先下
		// game.startTime = +new Date(); //开始计时
		waiting = false; //允许点击
	});
	
	//游戏
	game.steps = 0;
	game.startTime = +new Date();
	game.turn = 1;
	game.winner = false;
	clearInterval( game.ids[0] );
	clearInterval( game.ids[1] ); //时间暂停
	game.t0[0] = game.t0[1] = game.ids[0] = game.ids[1] = null;
	game.time[0].total = 900;
	game.time[0].step = 60;
	game.time[1].total = 900;
	game.time[1].step = 60;
	game.scores.now[0] = game.scores.now[1] =
		game.scores.goal[0] = game.scores.goal[1] = 0;
	game.scores.t0 = +new Date();
	game.scores.k = 0.005;
	game.lines = []; //清空分析画线
	
	//关闭工具
	scores(false);
	analyse(false);
	clearInterval(review_id); //停止复盘
	review_id = null;
	$("#tools > .review > .stop").hide();
	$("#tools > .review > .play").show();
	$("#toolBtn > .review").attr("disabled", "disabled");
	$("#tools > .review").slideUp("fast");
	
	//线程
	if (worker)
		worker.terminate(); //关闭线程
	worker = new Worker("./js/worker.js"); //多线程
	worker.postMessage({
		type: "init", //初始化
		cols,
		rows,
		depth,
		width,
		D: 4,
		N,
		mis: mistake
	});
	worker.onmessage = function(e){
		console.log("message: ", e.data.type, e.data)
		const type = e.data.type;
		if (workerCallback[type])
			workerCallback[type](e.data);
	};
	worker.onerror = function(e){
		console.error(`${e.message}\n\tat ${e.filename}:${e.lineno}`)
	};
}
//setTimeout(init, 1000); //等待淡入动画结束



/* socket */
if (typeof io == "undefined"){ //未加载socket.io.js
	$("#state").html("网络错误，请检查网络连接再刷新页面！")
		.removeClass().addClass("state failed");
	
}else{
	socket = io("https://wzh.glitch.me/");
	
	send = function (name, ...data){
		console.log("↑", name, data);
		socket.emit(name, ...data);
	};
	
	socket.on("connecting", function(data){
		$("#state").html("连接服务器中……")
			.removeClass().addClass("state connecting");
	});
	socket.on("connect", function(data){
		send("type", "gobang"); //type
		send("gobang_game_room", {
			oldID,
			room: ROOM,
			type: TYPE,
			first: FIRST
		}); //加入游戏房间
		$(".state").html("服务器连接成功")
			.removeClass().addClass("state success");
		layer.msg("<span>服务器连接成功</span>", {icon:1});
	});
	socket.on("connect_failed", function(data){
		$(".state").html("服务器连接失败")
			.removeClass().addClass("state failed");
		layer.msg("<span>服务器连接失败</span>", {icon:2});
	});
	socket.on("disconnect", function(data){
		$(".state").html("与服务器连接断开")
			.removeClass().addClass("state failed");
		layer.msg("<span>与服务器连接断开</span>", {icon:2});
		
		if (game.winner !== false) return; //游戏已结束
		if (game.first === null) return; //未开始游戏
		game.over(false, "与服务器连接断开，游戏失败");
	});
	socket.on("reconnecting", function(data){
		$(".state").html("重连中……")
			.removeClass().addClass("state connecting");
	});
	socket.on("reconnect", function(data){
		send("type", "gobang"); //type
		send("gobang_game_room", {
			oldID,
			room: ROOM,
			type: TYPE,
			first: FIRST
		}); //加入游戏房间
		$(".state").html("服务器重连成功")
			.removeClass().addClass("state success");
		layer.msg("<span>服务器重连成功</span>", {icon:1});
	});
	socket.on("reconnect_failed", function(data){
		$(".state").html("服务器重连失败")
			.removeClass().addClass("state failed");
		layer.msg("<span>服务器重连失败</span>", {icon:2});
	});
	socket.on("error", function(data){
		console.error(data);
		$(".state").html("服务器连接错误！")
			.removeClass().addClass("state failed");
		layer.msg("<span>服务器连接错误！</span>", {icon:2});
	});
	
	
	//分配新ID
	socket.on("gobang_assignID", function(data){
		console.log("↓ gobang_assignID", data)
		ID = data;
		$("#main > div.self > div > p.name").html(oldID);
		$("#state").html("<span>等待对方加入中……</span>");
		output("<span>id分配成功，等待对方加入中……</span>", true);
	});
	
	//准备好了
	socket.on("gobang_game_ready", function(data){
		console.log("↓ gobang_game_ready", data)
		
		game.first = (data.first && TYPE) || //房主 且 房主先手
			(!data.first && !TYPE); //成员 且 房主后手
		
		if (game.first){
			$("#main > div.self").removeClass("white").addClass("black");
			$("#main > div.opposite").removeClass("black").addClass("white");
			$("#main > div.self > div > p.name").html( "："+data.oldID[1-TYPE] );
			$("#main > div.opposite > div > p.name").html( "："+data.oldID[+TYPE] );
			layer.msg("你执黑棋，对方执白棋");
			$("#state").html("游戏开始，你执黑棋，对方执白棋");
			output("游戏开始，你执黑棋，对方执白棋", true);
			
		}else{
			$("#main > div.self").removeClass("black").addClass("white");
			$("#main > div.opposite").removeClass("white").addClass("black");
			$("#main > div.self > div > p.name").html( "："+data.oldID[1-TYPE] );
			$("#main > div.opposite > div > p.name").html( "："+data.oldID[+TYPE] );
			layer.msg("你执白棋，对方执黑棋");
			$("#state").html("游戏开始，你执白棋，对方执黑棋");
			output("游戏开始，你执白棋，对方执黑棋", true);
		}
		init();
		game.set(1); //黑棋先下
		game.startTime = +new Date(); //开始计时
	});
	
	//对方离开
	socket.on("gobang_game_leave", function(old_id){
		console.log("↓ gobang_game_leave", old_id)
		
		if (old_id != oldID){
			$("#state").html(`<span>对方</span>“${old_id}”<span>退出了房间</span>`);
			output(`<span>对方</span>“${old_id}”<span>退出了房间</span>`, true);
		}
		
		if (game.winner !== false) return; //游戏已结束
		game.over(true, `<span>对方</span>“${old_id}”<span>退出了房间，游戏结束！</span>`);
	});
	
	//下棋
	socket.on("gobang_game_play", function(data={}){
		console.log("↓ gobang_game_play", data)
		const {pos, side, time, totalTime} = data;
		
		const delay = (new Date()-time)/1000; //延时
		game.time[game.turn].total = totalTime; //局时
		game.time[game.turn].step += delay;
		
		$("#state").html("<span>下棋延时：</span>" + delay*1000 + "ms")
		if (side != game.first+1)
			game.play(pos.i, pos.j, pos.k, pos.l, false); //直接下 跳过防误触
	});
	
	//超时
	socket.on("gobang_game_timeout", function(old_id){
		console.log("↓ gobang_game_timeout", old_id)
		
		if (game.winner !== false) return; //游戏已结束
		game.over(true, `<span>对方</span>“${old_id}”<span>超时，游戏结束！</span>`);
	});
	
	//认输
	socket.on("gobang_game_giveUp", function(old_id){
		console.log("↓ gobang_game_giveUp", old_id)
		
		if (game.winner !== false) return; //游戏已结束
		game.over(true, `<span>对方</span>“${old_id}”<span>认输，游戏结束！</span>`);
	});
	
	//求和
	socket.on("gobang_game_drawMatch", function(old_id){
		console.log("↓ gobang_game_drawMatch", old_id)
		
		if (game.winner !== false) return; //游戏已结束
		
		layer.confirm(`<span>对方</span>“${old_id}”<span>请求求和，是否同意？</span>`, {
			btn: ["同意", "拒绝"],
			btn1: function(index){
				layer.close(index);
				send("gobang_game_drawMatch_agree", ROOM, oldID);
				output("你同意了对方的求和请求", true);
				game.over(null, "求和成功，游戏结束");
			},
			btn2: function(index){
				layer.close(index);
				send("gobang_game_drawMatch_disagree", ROOM, oldID);
				output("你拒绝了对方的求和请求", true);
			}
		});
	});
	
	//同意求和
	socket.on("gobang_game_drawMatch_agree", function(old_id){
		console.log("↓ gobang_game_drawMatch_agree", old_id)
		
		output("对方同意了你的求和请求", true);
		game.over(null, "求和成功，游戏结束");
	});
	
	//拒绝求和
	socket.on("gobang_game_drawMatch_disagree", function(old_id){
		console.log("↓ gobang_game_drawMatch_disagree", old_id)
		
		layer.msg("对方拒绝了你的求和请求");
		output("对方拒绝了你的求和请求", true);
	});
	
	//再来一局
	socket.on("gobang_game_replay", function(old_id){
		console.log("↓ gobang_game_replay", old_id)
		
		layer.confirm(`<span>对方</span>“${old_id}”<span>请求再来一局，是否同意？</span>`, {
			btn: ["同意", "拒绝"],
			btn1: function(index){
				layer.close(index);
				send("gobang_game_replay_agree", ROOM, oldID);
				
				init();
				send("gobang_game_room", {
					oldID,
					room: ROOM,
					type: TYPE,
					first: FIRST
				}); //加入游戏房间
				output("你同意了对方的再来一局请求", true);
			},
			btn2: function(index){
				layer.close(index);
				send("gobang_game_replay_disagree", ROOM, oldID);
				output("你拒绝了对方的再来一局请求", true);
			}
		});
	});
	
	//同意再来一局
	socket.on("gobang_game_replay_agree", function(old_id){
		console.log("↓ gobang_game_replay_agree", old_id)
		
		layer.msg("对方同意了你再来一局的请求");
		output("对方同意了你再来一局的请求", true);
		init();
		send("gobang_game_room", {
			oldID,
			room: ROOM,
			type: TYPE,
			first: FIRST
		}); //加入游戏房间
	});
	
	//拒绝再来一局
	socket.on("gobang_game_replay_disagree", function(old_id){
		console.log("↓ gobang_game_replay_disagree", old_id)
		
		layer.msg("对方拒绝了你再来一局的请求");
		output("对方拒绝了你再来一局的请求", true);
	});
	
	//聊天
	socket.on("gobang_game_chat", function(old_id, msg){
		console.log("↓ gobang_game_chat", old_id, msg)
		
		output(`${old_id}：${msg}`);
		
		if (old_id == oldID){ //自己发送
			layer.tips(msg, $("#main > .self > i")[0], {tips: 1}); //上方
		}else{ //对方发送
			layer.tips(msg, $("#main > .opposite > i")[0], {tips: 1}); //上方
		}
	});
	
	
	//下棋失败
	socket.on("gobang_game_play_failed", function(){
		console.log("↓ gobang_game_play_failed", room)
		layer.msg("<span>下棋失败，房间已被关闭</span>", {icon: 5});
		output("下棋失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//超时失败
	socket.on("gobang_game_timeout_failed", function(){
		console.log("↓ gobang_game_timeout_failed", room)
		layer.msg("<span>发送超时失败，房间已被关闭</span>", {icon: 5});
		output("发送超时失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败（超时）
	});
	
	//认输失败
	socket.on("gobang_game_giveUp_failed", function(){
		console.log("↓ gobang_game_giveUp_failed", room)
		layer.msg("<span>发送认输失败，房间已被关闭</span>", {icon: 5});
		output("发送认输失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败（认输）
	});
	
	//求和失败
	socket.on("gobang_game_drawMatch_failed", function(){
		console.log("↓ gobang_game_drawMatch_failed", room)
		layer.msg("<span>发送求和失败，房间已被关闭</span>", {icon: 5});
		output("发送求和失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//同意求和失败
	socket.on("gobang_game_drawMatch_agree_failed", function(){
		console.log("↓ gobang_game_drawMatch_agree_failed", room)
		layer.msg("<span>同意求和失败，房间已被关闭</span>", {icon: 5});
		output("同意求和失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//拒绝求和失败
	socket.on("gobang_game_drawMatch_disagree_failed", function(){
		console.log("↓ gobang_game_drawMatch_disagree_failed", room)
		layer.msg("<span>拒绝求和失败，房间已被关闭</span>", {icon: 5});
		output("拒绝求和失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//再来一局失败
	socket.on("gobang_game_replay_failed", function(){
		console.log("↓ gobang_game_replay_failed", room)
		layer.msg("<span>发送再来一局失败，房间已被关闭</span>", {icon: 5});
		output("发送再来一局失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//同意再来一局失败
	socket.on("gobang_game_replay_agree_failed", function(){
		console.log("↓ gobang_game_replay_agree_failed", room)
		layer.msg("<span>同意再来一局失败，房间已被关闭</span>", {icon: 5});
		output("同意再来一局失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//拒绝再来一局失败
	socket.on("gobang_game_replay_disagree_failed", function(){
		console.log("↓ gobang_game_replay_disagree_failed", room)
		layer.msg("<span>拒绝再来一局失败，房间已被关闭</span>", {icon: 5});
		output("拒绝再来一局失败，房间已被关闭", true);
		
		if (game.winner === false)
			game.winner = 1-game.first; //自己失败
	});
	
	//chat失败
	socket.on("gobang_chat_failed", function(room){
		console.log("↓ gobang_chat_failed", room)
		layer.msg("<span>发送聊天失败，房间已被关闭</span>", {icon: 5});
		output("发送聊天失败，房间已被关闭");
	});
	
}



//请求线程
function request(data){
	console.log("post: ", data.type, data)
	worker.postMessage(data);
	return {
		then(func){
			workerCallback[data.type] = func;
		}
	};
}


let moveTimes = 0; //移动次数
if (navigator.device > 0) //电脑
	setTimeout(()=>{ //等待loadScene
		board.scene.onPointerObservable.add(function(e){
			if (e.type == 1){ //mousedown
				moveTimes = 0; //新一次点击
				console.log("mousedown", validClick)
			}else if (e.type == 4){ //mousemove
				moveTimes++; //不允许移动
				console.log("mousemove", validClick)
			}
		});
	}, 600);
$("#board").click(function(e){
	console.log("click", moveTimes)
	if (moveTimes > 5) return; //无效点击
	if (!worker) return; //未初始化线程
	if (game.first != game.turn) return; //未轮到自己
	if (game.winner !== false) return; //已结束
	if (waiting) return; //上次未响应
	
	const {offsetX: x, offsetY: y} = e,
		pickResult = board.scene.pick(x, y);
	if (!pickResult.hit) return; //未点击到物体
	if ( !board.isBlock(pickResult.pickedMesh) ) return; //不是占位符和防误触
	
	playEffect(); //播放音效
	
	const pos = pickResult.pickedMesh.position,
		[i, j, k] = board.xyz2ijk(pos.x, pos.y, pos.z),
		l = board.widthRange;
	console.log("click", pickResult, pos, [i, j, k, l])
	
	waiting = true; //开始等待
	game.play(i, j, k, l, pickResult.pickedMesh);
});



/* header */
//返回
$("#back").click(function(){
	go("index.html");
});

//重来
$("#restart").click(function(){
	if (game.winner === false) //游戏未结束
		return layer.msg("游戏未结束，无法请求再来一局，可以先求和/认输");
	layer.confirm("确定请求再来一局吗？", function(index){
		layer.close(index);
		send("gobang_game_replay", ROOM, oldID);
		layer.msg("请求再来一局中……");
		output("你发送了再来一局请求", true);
	});
});


/* control */
$("#layerControl > .add").click(function(){
	board.layerAdd();
});
$("#layerControl > .sub").click(function(){
	board.layerSub();
});
$("#heightControl > .add").click(function(){
	board.heightAdd();
});
$("#heightControl > .sub").click(function(){
	board.heightSub();
});


/* axis */
const touch = {
	x: null,
	y: null
};
$("#axis").on("touchstart", function(e){
	const {clientX: x, clientY: y} = e.originalEvent.targetTouches[0]; 
	console.log("touchstart", x, y);
	touch.x = x,
	touch.y = y;
	
	e.preventDefault(); //阻止默认行为
	e.stopPropagation(); //阻止冒泡
	e.cancelBubble = true; //阻止冒泡
	return false;
});
$("#axis").on("touchmove", function(e){
	e.preventDefault(); //阻止默认行为
	e.stopPropagation(); //阻止冒泡
	e.cancelBubble = true; //阻止冒泡
	return false;
});
$("#axis").on("touchend", function(e){
	const {clientX: x, clientY: y} = e.originalEvent.changedTouches[0],
		dx = x - touch.x,
		dy = y - touch.y;
	console.log("touchend", x, y, dx, dy);
	
	if (Math.abs(dx) < 30){
		if (dy > 40){
			board.widthAdd();
		}else if (dy < -40){
			board.widthSub();
		}else if (dx == 0 && dy == 0 && e.target.tagName == "LI"){ //点击
			console.log(e.target, e.target.tagName, $(e.target).prevAll().length)
			board.setWidth($(e.target).prevAll().length);
		}
	}
	touch.x = touch.y = null;
	
	e.preventDefault(); //阻止默认行为
	e.stopPropagation(); //阻止冒泡
	e.cancelBubble = true; //阻止冒泡
	return false;
});


/* tools */
//认输
$("#toolBtn > .giveUp").click(function(){
	if (game.winner !== false) //游戏已结束
		return layer.msg("游戏已结束，无法认输");
	layer.confirm("真的要认输吗？", function(index){
		layer.close(index);
		send("gobang_game_giveUp", ROOM, oldID);
		game.over(false, "你认输了，游戏结束");
	});
});

//求和
$("#toolBtn > .drawMatch").click(function(){
	if (game.winner !== false) //游戏已结束
		return layer.msg("游戏已结束，无法求和");
	layer.confirm("确定请求求和吗？", function(index){
		layer.close(index);
		send("gobang_game_drawMatch", ROOM, oldID);
		layer.msg("请求求和中……");
		output("你发送了求和请求", true);
	})
});

//评分
function scores(display){
	const btn = $("#toolBtn > .scores"),
		tool = $("#tools > .scores");
	if ( display === true || //欲显示
		(display === undefined && tool.css("display") == "none") //已隐藏
	){
		clearInterval( scores_id ); //停止更新
		scores_id = setInterval(()=>game.updateScores(), 30); //更新分数
		
		btn.attr("disabled", "disabled");
		tool.slideDown("slow", ()=>btn.removeAttr("disabled"));
		
	}else{
		clearInterval( scores_id ); //停止更新
		scores_id = null;
		
		btn.attr("disabled", "disabled");
		tool.slideUp("slow", ()=>btn.removeAttr("disabled"));
	}
}
$("#toolBtn > .scores").click(function(){
	if ( $("#tools > .review").css("display") != "none" ){
		scores(false);
		return layer.msg("复盘时暂不支持棋局评分");
	}
	scores();
});

//分析
function analyse(display){
	if ( display === false || //欲隐藏
		(display === undefined && game.linesDisplay) //已显示
	){
		if (game.linesDisplay) //已显示
			layer.msg("棋局分析已关闭");
		game.updateLines(false);
	}else{
		if (!game.linesDisplay) //已隐藏
			layer.msg("棋局分析已开启");
		game.updateLines(true);
	}
}
$("#toolBtn > .analyse").click(function(){
	if ($("#tools > .review").css("display") != "none"){
		analyse(false);
		return layer.msg("复盘时暂不支持棋局分析");
	}
	analyse();
});

//重放
let reviewRestore; //重置函数
$("#toolBtn > .review").click(function(){
	if ( $("#tools > .review").css("display") == "none" ){ //需显示
		$(this).attr("disabled", "disabled");
		$("#tools > .review").slideToggle("slow", ()=>$(this).removeAttr("disabled"));
		
		scores(false); //关闭棋局评分
		analyse(false); //关闭棋局分析
		$("#tools > .review > .stop").hide();
		$("#tools > .review > .play").show(); //暂停
		
		request({
			type: "review" //重放
		}).then((data)=>{
			const {steps, pieces} = data;
			board.clear(); //清空棋子
			board.visibleLines(false, "win"); //隐藏获胜划线
			board.clearLines("analyse"); //删除分析划线
			const position = [...board.lines.update.circle]; //记录最后棋子位置
			
			let index = -1;
			
			const next = ()=>{
					if (index >= steps.length-1){
						board.visibleLines(true, "win"); //显示获胜划线
						return layer.msg("不能再往后了");
					}
					const {i, j, k, l, v} = steps[++index];
					board.set(i, j, k, l, v==2? "black": "white");
					board.moveCircle([i, j, k, l], 5, [0,1,0]);
					board.setWidth(l);
					return false;
				},
				last = ()=>{
					if (index <= 0)
						return layer.msg("不能再往前了");
					
					let {i, j, k, l, v} = steps[index--];
					board.set(i, j, k, l, "none");
					board.moveCircle([steps[index].i, steps[index].j, steps[index].k, steps[index].l], 5, [0,1,0]);
					board.setWidth(steps[index].l);
					
					board.visibleLines(false, "win"); //隐藏获胜划线
					return false;
				};
			reviewRestore = ()=>{
				for (let i=board.columns-1; i>=0; i--)
					for (let j=board.rows-1; j>=0; j--)
						for (let k=board.depth-1; k>=0; k--)
							for (let l=board.width-1; l>=0; l--){
								switch (pieces[i][j][k][l]){
									case 0:
										board.set(i, j, k, l, "none");
										break;
									case 1:
										board.set(i, j, k, l, "white");
										break;
									case 2:
										board.set(i, j, k, l, "black");
										break;
								}
							}
				board.visibleLines(true, "win"); //显示获胜划线
				board.moveCircle(position); //还原最后棋子位置
			};
			
			next();
			
			$("#tools > .review > .last").click(last);
			$("#tools > .review > .next").click(next);
			
			$("#tools > .review > .play").click(function(){
				$(this).hide();
				$("#tools > .review > .stop").show();
				
				if (review_id)
					clearInterval(review_id);
				
				review_id = setInterval(()=>{
					if (next() === false) return;
					clearInterval(review_id);
					review_id = null;
					$(this).show();
					$("#tools > .review > .stop").hide();
				}, 2000);
			});
			$("#tools > .review > .stop").click(function(){
				clearInterval(review_id);
				review_id = null;
				$(this).hide();
				$("#tools > .review > .play").show();
			});
		});
		
	}else{
		clearInterval(review_id);
		review_id = null;
		reviewRestore(); //重置
		
		$("#tools > .review > .stop").hide();
		$("#tools > .review > .play").show();
		$(this).attr("disabled", "disabled");
		$("#tools > .review").slideToggle("slow", ()=>$(this).removeAttr("disabled"));
	}
});


/* 发送消息 */
$("#info > .input > button").click(chat);
function chat(){
	send("gobang_game_chat", ROOM, oldID, $("#info > .input > input").val());
	$("#info > .input > input").val(""); //清空输入
	return false;
}
function output(text, center=false){
	$("#info > .output").append(
		$("<p></p>").html( text )
			.css("text-align", center? "center": "")
	);
}


/* 电脑版按键 */
document.onkeydown = function(e){
	console.log(e.key)
	if (e.key == "Escape"){
		go("index.html");
	}else if (e.key == "F2"){ //评分
		if (!$("#toolBtn > .scores").attr("disabled"))
			scores();
	}else if (e.key == "F3"){ //分析
		if (!$("#toolBtn > .analyse").attr("disabled"))
			analyse();
	}else if (e.key == "F4"){ //复盘
		if (!$("#toolBtn > .review").attr("disabled"))
			review();
	}else if (e.key == "F5"){ //再来一局
		init();
	}else if (e.key == "ArrowUp"){ //棋盘向上
		board.heightAdd();
	}else if (e.key == "ArrowDown"){ //棋盘向下
		board.heightSub();
	}else if (e.key == "ArrowLeft"){ //棋盘缩小
		board.layerSub();
	}else if (e.key == "ArrowRight"){ //棋盘放大
		board.layerAdd();
	}else if (e.key == "PageUp"){ //W轴向上
		board.widthSub();
	}else if (e.key == "PageDown"){ //W轴向下
		board.widthAdd();
	}
	e.preventDefault();
	return false;
};
</script>
		
	</body>
</html>
